// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace SnapObjects.Data
{
    internal class ModelTrackable<TModel>
        : IDetailTracker<TModel>
    {
        private readonly IMapperContext _mapperContext;
        private readonly IMapperChangeTracker _changeTracker;
        private readonly Dictionary<string, object> _trackedDetailEntries;

        public ModelTrackable(
            IMapperContext mapperContext,
            IMapperChangeTracker changeTracker)
        {
            _mapperContext = mapperContext;
            _changeTracker = changeTracker;
            _trackedDetailEntries = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
        }

        internal EntryTracker<TModel> MasterEntryTracker { get; private set; }

        /// <summary>
        ///     Gets a reference to the master model object.
        /// </summary>
        public TModel MasterModel => this.MasterEntryTracker.Current;

        //public IModelEntry MasterEntry => _masterEntry;

        /// <summary>
        ///     Updates the database by executing the data changes tracked by the ISqlModelMapper object.
        /// </summary>
        /// <returns>
        ///     Returns an IDbResult object which can be used to conveniently get the result of the executed database operation.
        /// </returns>
        public IDbResult SaveChanges()
        {
            return _mapperContext.ModelMapper.SaveChanges();
        }

        public IDetailTracker<TModel> TrackMaster(
            IModelEntry<TModel> modelEntry,
            Action<ISaveContext> beforeSaveAction,
            Action<ISaveContext> afterSaveAction)
        {
            this.MasterEntryTracker = new EntryTracker<TModel>(_mapperContext, modelEntry,
                                    false, beforeSaveAction, afterSaveAction);

            return this;
        }

        /// <summary>
        ///     Tracks an insert, update or delete operation on the table which the detail model is mapped to, 
        ///     when working with the master-detail models (where the ModelEmbedded attribute is applied in a 
        ///     property of the master model). The data state determines which type of operation to be performed. 
        ///     The data to be manipulated is cached in a TDetailModel object.When ISqlModelMapper.SaveChanges
        ///     method is called, a SQL statement (INSERT, UPDATE, or DELETE) will be first generated using the 
        ///     data cached in the TDetailModel object and the mapping information defined in TDetailModel, 
        ///     and then executed.
        /// </summary>
        /// <typeparam name="TDetailModel">The type of the detail model class.</typeparam>
        /// <param name="propertyExpr">
        ///     A property of the master model (TModel) where the ModelEmbedded attribute is applied.
        ///     The type of this property is TDetailModel.
        ///     This property is specified by an expression.
        /// </param>
        /// <param name="modelEntry">
        ///     An <see cref="IModelEntry{TDetailModel}"/> object which contains the data and data state and which will be tracked.
        /// </param>
        /// <returns>Returns the current <see cref="IDetailTracker{TModel}"/> object.</returns>
        public IDetailTracker<TModel> TrackDetail<TDetailModel>(
            Expression<Func<TModel, object>> propertyExpr,
            IModelEntry<TDetailModel> modelEntry)
        {
            var property = ModelHelper.ExtractProperty(propertyExpr).Name;

            if (!_trackedDetailEntries.ContainsKey(property))
            {
                this.TrackDetailInternal(this.MasterEntryTracker, this.MasterEntryTracker.Current, propertyExpr, modelEntry);
                _trackedDetailEntries.Add(property, modelEntry);
            }
            else
            {
                throw new InvalidOperationException("Duplicate detail tracking, property name: " + property);
            }

            return this;
        }

        /// <summary>
        ///     Tracks an insert, update or delete operation on the table which the granddetail model is
        ///     mapped to when working with the master-detail-granddetail models where the ModelEmbedded 
        ///     attribute is applied in a property of the master model and a property of the detail model. 
        ///     The data state determines which type of operation to be performed. The data to be manipulated 
        ///     is cached in a TGrandDetailModel object.When ISqlModelMapper.SaveChanges is called, a SQL 
        ///     statement (INSERT, UPDATE, or DELETE) will be generated using the data cached in the 
        ///     TGrandDetailModel object and the mapping information defined in TGrandDetailModel, 
        ///     and then the SQL statement will be executed.
        /// </summary>
        /// <typeparam name="TDetailModel">The type of the detail model class.</typeparam>
        /// <typeparam name="TGrandDetailModel">The type of the granddetail model class.</typeparam>
        /// <param name="propertyExpr">
        ///     A property of the master model (TModel) where the ModelEmbedded attribute is applied.
        ///     The type of this property is TDetailModel.
        ///     This property is specified by an expression.
        /// </param>
        /// <param name="detailPropertyExpr">
        ///     A property of the detail model where the ModelEmbedded attribute is applied.
        ///     The type of this property is TGrandDetailModel.
        ///     This property is specified by an expression.
        /// </param>
        /// <param name="grandModelEntry">
        ///     An <see cref="IModelEntry{TGrandDetailModel}"/> object that contains the data and data state and which will be tracked. 
        ///     The data corresponds to the property specified by detailPropertyExpr.
        /// </param>
        /// <returns>Returns the current <see cref="IDetailTracker{TModel}"/> object.</returns>
        public IDetailTracker<TModel> TrackGrandDetail<TDetailModel, TGrandDetailModel>(
            Expression<Func<TModel, object>> propertyExpr,
            Expression<Func<TDetailModel, object>> detailPropertyExpr,
            IModelEntry<TGrandDetailModel> grandModelEntry)
        {
            //Master model accessor
            var accessor = (ICompositeModelAccessor<TModel>)_mapperContext
                                .GetCompositeModelAccessor(typeof(TModel));

            var property = ModelHelper.ExtractProperty(propertyExpr).Name;
            var detailModel = (TDetailModel)accessor.GetValue(this.MasterModel, property);

            if (_trackedDetailEntries.TryGetValue(property, out var detailEntry))
            {
                this.TrackDetailInternal((IModelEntry<TDetailModel>)detailEntry, detailModel,
                                        detailPropertyExpr, grandModelEntry);
            }
            else
            {
                throw new InvalidOperationException("Detail is not tracked and call TrackDetail(...) first, property name: " + property);
            }

            return this;
        }

        /// <summary>
        ///     Tracks an insert, update or delete operation on the table which the granddetail model is mapped to 
        ///     when working with the master-detail-granddetail models where the ModelEmbedded attribute is applied 
        ///     in a property of the master model and a property of the detail model. The data state determines which
        ///     type of operation to be performed. The data to be manipulated is cached in a sequence of TGrandDetailModel 
        ///     objects.When ISqlModelMapper.SaveChanges is called, a SQL statement (INSERT, UPDATE, or DELETE) will 
        ///     be generated using the data cached in TGrandDetailModel and the mapping information defined in 
        ///     TGrandDetailModel, and then the SQL statement will be executed.
        /// </summary>
        /// <typeparam name="TDetailModel">The type of the detail model class.</typeparam>
        /// <typeparam name="TGrandDetailModel">The type of the granddetail model class.</typeparam>
        /// <param name="propertyExpr">
        ///     A property of the master model where the ModelEmbedded attribute is applied.
        ///     The type of this property is TDetailModel.
        ///     This property is specified by an expression.
        /// </param>
        /// <param name="detailPropertyExpr">
        ///     A property of the detail model where the ModelEmbedded attribute is applied.
        ///     The type of this property is a collection of TGrandDetailModel.
        ///     This property is specified by an expression.
        /// </param>
        /// <param name="grandModelEntries">
        ///     An <see cref="IEnumerable{IModelEntry}"/> object which contains the data and data state and 
        ///     which will be tracked. The data corresponds to the second property expression.
        /// </param>
        /// <returns>Returns the current <see cref="IDetailTracker{TModel}"/> object.</returns>
        public IDetailTracker<TModel> TrackGrandDetails<TDetailModel, TGrandDetailModel>(
            Expression<Func<TModel, object>> propertyExpr,
            Expression<Func<TDetailModel, object>> detailPropertyExpr,
            IEnumerable<IModelEntry<TGrandDetailModel>> grandModelEntries)
        {
            //Master model accessor
            var accessor = (ICompositeModelAccessor<TModel>)_mapperContext
                                .GetCompositeModelAccessor(typeof(TModel));

            var property = ModelHelper.ExtractProperty(propertyExpr).Name;
            var detailModel = (TDetailModel)accessor.GetValue(this.MasterModel, property);

            if (_trackedDetailEntries.TryGetValue(property, out var detailEntry))
            {
                this.TrackDetailsInternal((IModelEntry<TDetailModel>)detailEntry, detailModel,
                                        detailPropertyExpr, grandModelEntries);
            }
            else
            {
                throw new InvalidOperationException("Detail is not tracked and call TrackDetail(...) first, property name: " + property);
            }

            return this;
        }

        /*public IDetailTracker<TModel> TrackDetailAndGrandDetail<TDetailModel, TGrandDetailModel>(
            Expression<Func<TModel, object>> propertyExpr,
            IModelEntry<TDetailModel> modelEntry,
            Expression<Func<TDetailModel, object>> detailPropertyExpr,
            IModelEntry<TGrandDetailModel> grandModelEntry)
        {
            var statefulModel = TrackDetailInternal(MasterEntryTracker, 
                                    MasterEntryTracker.Current, propertyExpr, modelEntry);

            TrackDetailInternal(modelEntry, (TDetailModel)statefulModel.Current, 
                                    detailPropertyExpr, grandModelEntry);

            return this;
        }

        public IDetailTracker<TModel> TrackDetailAndGrandDetails<TDetailModel, TGrandDetailModel>(
            Expression<Func<TModel, object>> propertyExpr,
            IModelEntry<TDetailModel> modelEntry,
            Expression<Func<TDetailModel, object>> detailPropertyExpr,
            IEnumerable<IModelEntry<TGrandDetailModel>> grandModelEntries)
        {
            var statefulModel = TrackDetailInternal(MasterEntryTracker, 
                                    MasterEntryTracker.Current, propertyExpr, modelEntry);

            TrackDetailsInternal(modelEntry, (TDetailModel)statefulModel.Current,
                                    detailPropertyExpr, grandModelEntries);

            return this;
        }*/

        private IStatefulModel TrackDetailInternal<TParentModel, TDetailModel>(
                IModelEntry parentEntry,
                TParentModel parentModel,
                Expression<Func<TParentModel, object>> propertyExpr,
                IModelEntry<TDetailModel> modelEntry)
        {
            var property = ModelHelper.ExtractProperty(propertyExpr).Name;

            var accessor = (ICompositeModelAccessor<TParentModel>)_mapperContext
                                .GetCompositeModelAccessor(typeof(TParentModel));

            if (!((PocoModelMeta)accessor.ModelMeta).IsEmbedded(property))
            {
                throw new InvalidOperationException("Property is not a embedded property,  property name: " + property);
            }

            var statefulModel = this.TrackDetailInternal<TParentModel, TDetailModel>(
                                    parentEntry, property, modelEntry, false);

            //MasterEntryTracker.SetEmbeddedValue(property, statefulModel.Current);
            accessor.SetValue(parentModel, property, statefulModel.Current);

            return statefulModel;
        }

        /// <summary>
        /// Tracks one or more insert, update or delete operations on the table which the detail model 
        /// is mapped to, when working with the master-detail models (where the ModelEmbedded attribute 
        /// is applied in a property of the master model). The data state determines which type of operation 
        /// to be performed. The data to be manipulated is cached in a sequence of TDetailModel objects.
        /// When ISqlModelMapper.SaveChanges is called, one or more SQL statements (INSERT, UPDATE, or DELETE) 
        /// will be first generated using the data cached in TDetailModel objects and the mapping information 
        /// defined in TDetailModel, and then executed.
        /// </summary>
        /// <typeparam name="TDetailModel">The type of the detail model class.</typeparam>
        /// <param name="propertyExpr">
        ///     A property of the master model (TModel) where the ModelEmbedded attribute is applied.
        ///     The type of this property is a collection of TDetailModel.
        ///     This property is specified by an expression.
        /// </param>
        /// <param name="modelEntries">An <see cref="IEnumerable{IModelEntry}"/> object that contains the data and data state which will be tracked.</param>
        /// <returns>Returns the current <see cref="IDetailTracker{TModel}"/> object.</returns>
        public IDetailTracker<TModel> TrackDetails<TDetailModel>(
            Expression<Func<TModel, object>> propertyExpr,
            IEnumerable<IModelEntry<TDetailModel>> modelEntries)
        {
            return this.TrackDetailsInternal(this.MasterEntryTracker, this.MasterEntryTracker.Current,
                                    propertyExpr, modelEntries, false);
        }

        private IDetailTracker<TModel> TrackDetailsInternal<TParentModel, TDetailModel>(
            IModelEntry parentEntry,
            TParentModel parentModel,
            Expression<Func<TParentModel, object>> propertyExpr,
            IEnumerable<IModelEntry<TDetailModel>> modelEntries,
            bool changedOnly = false)
        {
            var property = ModelHelper.ExtractProperty(propertyExpr).Name;

            var accessor = (ICompositeModelAccessor<TParentModel>)_mapperContext
                                .GetCompositeModelAccessor(typeof(TParentModel));
            var parentMeta = (PocoModelMeta)accessor.ModelMeta;

            if (!parentMeta.IsEmbedded(property))
            {
                throw new InvalidOperationException("Property is not an embedded property,  property name: " + property);
            }

            var models = new List<TDetailModel>();

            foreach (var modelEntry in modelEntries)
            {
                var statefulModel = this.TrackDetailInternal<TParentModel, TDetailModel>(
                                        parentEntry, property, modelEntry, changedOnly);

                models.Add((TDetailModel)statefulModel.Current);
            }

            if (parentMeta.IsContainerType(property))
            {
                //parentTracker.SetEmbeddedValue(property, models);
                accessor.SetValue(parentModel, property, models);
            }
            else if (parentMeta.IsArray(property))
            {
                //parentTracker.SetEmbeddedValue(property, models.ToArray());
                accessor.SetValue(parentModel, property, models.ToArray());
            }
            else
            {
                throw new InvalidOperationException("Uncompatiable data type, container type or array needed, property: " + property);
            }

            return this;
        }

        private IStatefulModel TrackDetailInternal<TParentModel, TDetailModel>(
            IModelEntry parentEntry,
            string property,
            IModelEntry<TDetailModel> modelEntry,
            bool changedOnly)
        {
            //var modelFactory = ModelFactoryFactory.Create<TDetailModel>();
            //var statefulModel = EntryStatefulModelFactory.Create(modelFactory, modelEntry);
            var needTrack = false;
            var isCascadeModel = false;

            var accessor = (ICompositeModelAccessor<TParentModel>)_mapperContext
                                .GetCompositeModelAccessor(typeof(TParentModel));
            var detailAccessor = (ICompositeModelAccessor<TDetailModel>)_mapperContext
                    .GetCompositeModelAccessor(typeof(TDetailModel));
            var statefulModel = EntryStatefulModelFactory.Create(detailAccessor, modelEntry, ChangeTrackingStrategy.Snapshot);

            if (parentEntry.ModelState == ModelState.New ||
                parentEntry.ModelState == ModelState.NewModified)
            {
                if (((PocoModelMeta)accessor.ModelMeta).IsCascadeCreate(property))
                {
                    if (statefulModel.ModelState != ModelState.New &&
                        statefulModel.ModelState != ModelState.NewModified)
                    {
                        throw new InvalidOperationException("ModelState needs to be New or NewModified for cascade create, current ModelState: " + statefulModel.ModelState);
                    }
                    else
                    {
                        isCascadeModel = true;
                    }
                }
                else
                {
                    needTrack = true;
                }
            }
            else if (parentEntry.ModelState == ModelState.Deleted)
            {
                if (((PocoModelMeta)accessor.ModelMeta).IsCascadeDelete(property))
                {
                    if (statefulModel.ModelState != ModelState.Deleted)
                    {
                        throw new InvalidOperationException("ModelState needs to be Deleted for cascade delete, current ModelState: " + statefulModel.ModelState);
                    }
                    else
                    {
                        isCascadeModel = true;
                    }
                }
                else
                {
                    needTrack = true;
                }
            }
            else
            {
                needTrack = true;
            }

            if (changedOnly &&
                statefulModel.ModelState != ModelState.NewModified &&
                statefulModel.ModelState != ModelState.Modified &&
                statefulModel.ModelState != ModelState.Deleted)
            {
                needTrack = false;
            }

            if (needTrack || isCascadeModel)
            {
                this.TrackStateFulModel<TDetailModel>(statefulModel, isCascadeModel);
            }

            return statefulModel;
        }

        private void TrackStateFulModel<TDetailModel>(
            IStatefulModel statefulModel,
            bool isCascadeModel)
        {
            IModelTracker modelTracker = null;

            if (statefulModel.ModelState == ModelState.NewModified ||
                statefulModel.ModelState == ModelState.New)
            {
                modelTracker = new CreateTracker<TDetailModel>(_mapperContext,
                                            statefulModel, null, null);
            }
            else if (statefulModel.ModelState == ModelState.Modified ||
                statefulModel.ModelState == ModelState.NotModified)
            {
                modelTracker = new UpdateTracker<TDetailModel>(_mapperContext,
                                            statefulModel, null, null, false);
            }
            else if (statefulModel.ModelState == ModelState.Deleted)
            {
                modelTracker = new DeleteTracker<TDetailModel>(_mapperContext,
                                            statefulModel, null, null);
            }

            if (modelTracker != null)
            {
                _changeTracker.AddTracker((TDetailModel)statefulModel.Current,
                                            modelTracker, isCascadeModel);
            }
        }
    }
}
